// Copyright (c) 2023 Presto Labs Pte. Ltd.
// Author: qiang

#include "coin2/exchange/mercado/api_order/native_private_client.h"

#include "coin2/exchange/mercado/api_util/auth_key.h"
#include "coin2/exchange/mercado/api_util/common.h"

#define CHECK_API_LIMIT(action_type)                                                      \
  if (!HasEnoughRateLimit(ts, action_type)) {                                             \
    LOG_EVERY_N(WARNING, 50) << "Hit rate limit or api temporary disabled, " << __func__; \
    api_block_info_[__func__]++;                                                          \
    return {};                                                                            \
  }

MercadoNativePrivateClient::MercadoNativePrivateClient(
    const MarketExchangeApi& mea,
    const NativeClientConfig& config_override) {
  mea_ = mea;
  NativeClientConfig config;
  config.set_rest_host("api.mercadobitcoin.net");
  config.set_rest_host_port(443);
  config.MergeFrom(config_override);
  LOG(INFO) << "[MercadoNativePrivateClient] config: " << config.DebugString();

  auths_.Construct<MercadoSpotRestAuth, NativeClientConfig>(config);

  rate_tracker_ = std::make_unique<MercadoRateTracker>(mea_, config);
  local_rate_tracker_ = dynamic_cast<MercadoRateTracker*>(rate_tracker_.get());
  start_time_ = GetCurrentTimestamp();
}

std::shared_ptr<AsioHttpContext> MercadoNativePrivateClient::QueryAccountBalance() {
  if (account_id_.empty()) {
    LOG(ERROR) << "[QueryAccountBalance] Mercado account id not ready";
    return {};
  }

  auto ts = GetCurrentTimestamp();
  CHECK_API_LIMIT(ActionType::QUERY);
  UrlParamsBuilder builder;
  std::string path = "/api/v4/accounts/" + this->account_id_ + "/balances";
  auto* context = GetAuth()->CreateRequestByGetWithSignature(path, &builder);
  api_req_info_[__func__]++;
  local_rate_tracker_->Push(1, ts, ActionType::QUERY);

  context->query_type = OrderSubTopicId::REST_ACCOUNT_BALANCE;
  context->extra_info = DebugString();
  return std::shared_ptr<AsioHttpContext>(context);
}

std::string MercadoNativePrivateClient::ToExchangeOrderId(const std::string& og_exchange_id) {
  auto index = og_exchange_id.rfind('-') + 1;
  return og_exchange_id.substr(index, og_exchange_id.size() - index);
}

std::shared_ptr<AsioHttpContext> MercadoNativePrivateClient::ListOrder(const RestParams& params) {
  if (account_id_.empty()) {
    LOG(ERROR) << "[ListOrder] Mercado account id not ready";
    return {};
  }
  if (!params.has_symbol()) {
    return {};
  }

  auto ts = GetCurrentTimestamp();
  CHECK_API_LIMIT(ActionType::QUERY);
  UrlParamsBuilder builder;
  if (params.has_status()) {
    builder.PutGet("status", params.status());
  }
  if (params.has_side()) {
    builder.PutGet("side", params.side());
  }

  std::string path = "/api/v4/accounts/" + this->account_id_ + "/" + params.symbol() + "/orders";
  auto* context = GetAuth()->CreateRequestByGetWithSignature(path, &builder);
  api_req_info_[__func__]++;
  local_rate_tracker_->Push(1, ts, ActionType::QUERY);

  context->query_type = OrderSubTopicId::REST_ALL_ORDERS;
  context->extra_info = DebugString();

  return std::shared_ptr<AsioHttpContext>(context);
}

std::string MercadoNativePrivateClient::DebugString() const {
  double delta_t = (GetCurrentTimestamp() - start_time_) / 1.0e9;
  std::stringstream ss;
  ss << "MercadoNativePrivateClient::DebugString\n";
  ss << "api info\n";
  for (auto& [name, value] : api_req_info_) {
    int32_t blocked = 0;
    if (api_block_info_.count(name) > 0) {
      blocked = api_block_info_.at(name);
    }
    int32_t total_req = value + blocked;
    ss << name << " (total:" << total_req << ", succeeded: " << value << ", blocked: " << blocked
       << ", req_rate: " << value / delta_t << ", block_rate: " << blocked / delta_t << ")\n";
  }
  return ss.str();
}

auto MercadoNativePrivateClient::GetRestOrderParams(
    const OrderSpec& order_spec,
    int64_t proc_order_id) -> RestParams {
  auto params = RestParams();
  MercadoProduct product =
      MercadoProduct::FromStr(order_spec.product, order_spec.order_created_time);
  auto product_holder = GetProductHolder(product, order_spec.order_created_time);
  params.set_symbol(order_spec.product);
  params.set_client_order_id(std::to_string(proc_order_id));
  if (order_spec.order_side == OrderSide::BUY_ORDER) {
    params.set_side("buy");
  } else if (order_spec.order_side == OrderSide::SELL_ORDER) {
    params.set_side("sell");
  } else {
    NOTREACHED() << "Unknown order side: " << static_cast<int>(order_spec.order_side);
  }
  auto formatter = product_holder->product_info().order_formatter();
  params.set_qty(formatter.FormatQty(order_spec.qty));
  params.set_price(formatter.FormatPrice(order_spec.price));

  if (order_spec.post_only) {
    params.set_type("post-only");
  } else if (order_spec.order_type == OrderType::MARKET_ORDER) {
    params.set_type("market");
  } else {
    params.set_type("limit");
  }
  return params;
}

auto MercadoNativePrivateClient::GetRestCancelParams(const IOrderContext& order_context)
    -> RestParams {
  auto params = RestParams();
  if (order_context.has_exchange_order_id()) {
    params.set_order_id(ToExchangeOrderId(order_context.exchange_order_id()));
  } else {
    params.set_client_order_id(std::to_string(order_context.proc_order_id()));
  }

  params.set_symbol(order_context.native_product());
  return params;
}

std::shared_ptr<AsioHttpContext> MercadoNativePrivateClient::PlaceOrder(const RestParams& params) {
  if (account_id_.empty()) {
    LOG(ERROR) << "[PlaceOrder] Mercado account id not ready";
    return {};
  }
  if (!params.has_symbol()) {
    return {};
  }

  auto ts = GetCurrentTimestamp();
  CHECK_API_LIMIT(ActionType::PLACE);

  std::stringstream path;
  path << "/api/v4/accounts/" << this->account_id_ << "/" << params.symbol() << "/"
       << "orders";

  UrlParamsBuilder builder;
  // builder.PutPost("externalId", params.client_order_id());
  builder.PutPost<true>("limitPrice", std::stod(params.price()));
  builder.PutPost("qty", params.qty());
  builder.PutPost("side", params.side());
  builder.PutPost("type", params.type());

  auto* context = GetAuth()->CreateRequestByPostWithSignature(path.str(), &builder);
  local_rate_tracker_->Push(1, ts, ActionType::PLACE);
  api_req_info_[__func__]++;

  context->query_type = OrderSubTopicId::SUBMIT_RESPONSE;
  context->extra_info = DebugString();
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> MercadoNativePrivateClient::Accounts() {
  UrlParamsBuilder builder;
  auto* context = GetAuth()->CreateRequestByGetWithSignature("/api/v4/accounts", &builder);

  if (mb_) {
    const std::string ch_id = "mercado/accounts";
    if (!mb_->has_channel(ch_id)) {
      mb_->Subscribe(
          ch_id,
          std::bind(
              &MercadoNativePrivateClient::AccountCallBack,
              this,
              std::placeholders::_1,
              std::placeholders::_2));
    }
  }

  context->query_type = OrderSubTopicId::REST_ACCOUNTS;
  // context->extra_info = DebugString();
  return std::shared_ptr<AsioHttpContext>(context);
}

void MercadoNativePrivateClient::AccountCallBack(const std::string& channel, void* data) {
  account_id_ = *static_cast<std::string*>(data);
  LOG(INFO) << "[MercadoNativePrivateClient::AccountCallBack] Account info update " << account_id_;
  return;
}

void MercadoNativePrivateClient::TokenCallBack(const std::string& channel, void* data) {
  token* p_token = static_cast<token*>(data);
  LOG(INFO) << "[MercadoNativePrivateClient::TokenCallBack] Rest token update "
            << p_token->token_str << " expiration " << p_token->expiration << "  "
            << Iso8601FromTimestamp(p_token->expiration);

  auto* auth = GetAuth();
  static_cast<MercadoSpotRestAuth*>(auth)->SetToken(p_token->token_str);
  expiration_ = p_token->expiration;
  return;
}

std::shared_ptr<AsioHttpContext> MercadoNativePrivateClient::QueryToken() {
  UrlParamsBuilder builder;
  if (mb_) {
    const std::string ch_id = "meradobitcoin/token";
    if (!mb_->has_channel(ch_id)) {
      mb_->Subscribe(
          ch_id,
          std::bind(
              &MercadoNativePrivateClient::TokenCallBack,
              this,
              std::placeholders::_1,
              std::placeholders::_2));
    }
  }

  auto* auth = GetAuth();
  auto* context = static_cast<MercadoSpotRestAuth*>(auth)->TokenGenerator("/api/v4/authorize");
  context->query_type = OrderSubTopicId::LISTEN_KEY;
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> MercadoNativePrivateClient::CancelOrder(const RestParams& params) {
  if (account_id_.empty()) {
    LOG(ERROR) << "[CancelOrder] Mercado account id not ready";
    return {};
  }

  auto ts = GetCurrentTimestamp();
  CHECK_API_LIMIT(ActionType::CANCEL);

  std::stringstream path;
  path << "/api/v4/accounts/" << this->account_id_ << "/" << params.symbol() << "/orders/"
       << params.order_id();
  UrlParamsBuilder builder;
  auto* context = GetAuth()->CreateRequestByDeleteWithSignature(path.str(), &builder);
  local_rate_tracker_->Push(1, ts, ActionType::CANCEL);
  api_req_info_[__func__]++;

  context->query_type = OrderSubTopicId::CANCEL_RESPONSE;
  context->extra_info = DebugString();
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> MercadoNativePrivateClient::PlaceOrderNorm(
    const OrderExecution& order_exe) {
  const auto& order_spec = *order_exe.order_spec;
  const auto proc_order_id = order_exe.proc_order_id;
  auto params = GetRestOrderParams(order_spec, proc_order_id);
  return PlaceOrder(params);
}

std::shared_ptr<AsioHttpContext> MercadoNativePrivateClient::CancelOrderNorm(
    const OrderExecution& order_exe) {
  auto params = GetRestCancelParams(*order_exe.oc);
  return CancelOrder(params);
}

std::shared_ptr<AsioHttpContext> MercadoNativePrivateClient::SubmitOrderExecution(
    const OrderExecutionProto& order) {
  switch (order.type()) {
    case OrderExecutionProto::SUBMIT_ORDER: {
      auto order_spec = OrderSpec::FromProto(order.order_spec());
      auto params = GetRestOrderParams(*order_spec, order.proc_order_id());
      return PlaceOrder(params);
    }
    case OrderExecutionProto::CANCEL_ORDER: {
      auto order_spec = OrderSpec::FromProto(order.order_spec());
      auto params = GetRestOrderParams(*order_spec, order.proc_order_id());
      return CancelOrder(params);
    }
    default: {
      return nullptr;
    }
  }
}

std::shared_ptr<AsioHttpContext> MercadoNativePrivateClient::SubmitRestQuery(
    const RestQueryProto& query) {
  RestParams params;
  switch (query.type()) {
    case RestQueryProto::QUERY_ACCOUNT_BALANCE: {
      return QueryAccountBalance();
    }
    case RestQueryProto::QUERY_OPEN_ORDERS: {
      return {};
    }
    default: {
      return nullptr;
    }
  }
}
